﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Audio;
using System.IO;
using Microsoft.Xna.Framework.Input;
using MrDev.GameObjects;
using MrDev.Graphics;
using MrDev.GameMath;
using MrDev.Physics;

namespace MrDev
{
    /// <summary>
    /// A uniform grid of tiles with collections of gems and enemies.
    /// The level owns the player and controls the game's win and lose
    /// conditions as well as scoring.
    /// </summary>
    class Level : IDisposable
    {
        public List<Explosion> Explosions = new List<Explosion>();
        public List<Powerup> Powerups = new List<Powerup>();
        public delegate void BossScreenDelegate(GameObject objectSource, int triggerTileX, int triggerTileY);
        public ObjectPhysics Physics;

        // Physical structure of the level.
        private Tile[,] tiles;
        // The layer which entities are drawn on top of.
        private const int EntityLayer = 2;
        public List<Car> Enemies;
        public List<Platform> Platforms;
        Texture2D blocksTexture;
        public bool Paused = false;

        // Entities in the level.
        private Player player;
        private Boss boss;

        // Key locations in the level.        
        private Vector2 start;
        private Vector2 cameraCenter;
        public Vector2 CameraFixPosition;
        public bool FixedCamera;
        private bool cameraWasMoved;
        public Rectangle SubLevelSize = new Rectangle(0, 0, 20, 15);
        private int pixelScreenWidth;
        private int pixelScreenHeight;

        public Player Player
        {
            get { return player; }
        }

        public Boss Boss
        {
            get { return boss; }
        }

        public float ScreenX
        {
            get { return cameraCenter.X - pixelScreenWidth / 2f; }
        }

        public float ScreenY
        {
            get { return cameraCenter.Y - pixelScreenHeight / 2f; }
        }

        public Rectangle LevelSize
        {
            get
            {
                return new Rectangle(0, 0, tiles.GetUpperBound(0)+1, tiles.GetUpperBound(1)+1);
            }
        }


        // Level game state.
        private Random random = new Random(354668); // Arbitrary, but constant seed

        private const int PointsPerSecond = 5;

        // Level content.        
        public ContentManager Content
        {
            get { return content; }
        }
        ContentManager content;

        #region Loading

        /// <summary>
        /// Constructs a new level.
        /// </summary>
        /// <param name="serviceProvider">
        /// The service provider that will be used to construct a ContentManager.
        /// </param>
        /// <param name="path">
        /// The absolute path to the level file to be loaded.
        /// </param>
        public Level(IServiceProvider serviceProvider, string path)
        {
            // Create a new content manager to load content used just by this level.
            random = new Random(0);
            Physics = new ObjectPhysics();
            content = new ContentManager(serviceProvider, "Content");
            blocksTexture = Content.Load<Texture2D>("Tiles/tiles");
            pixelScreenWidth = Tile.Width * (int)SubLevelSize.Width;
            pixelScreenHeight = Tile.Height * (int)SubLevelSize.Height;
            Enemies = new List<Car>();
            Platforms = new List<Platform>();
            LoadLevel(path);
        }

        private void LoadLevel(string path)
        {
            string levelDir = Path.GetDirectoryName(path);
            string[] lines = File.ReadAllLines(path);
            int width = lines[0].Length;

            // Allocate the tile grid.
            Width = width * (int)SubLevelSize.Width;
            Height = lines.Length * (int)SubLevelSize.Height;
            tiles = new Tile[Width, Height];
            for (int y = 0; y < lines.Length; y++)
            {
                string line = lines[y];
                for (int x = 0; x < line.Length; x++)
                {
                    string subLevelPath = Path.Combine(levelDir, line[x] + ".txt");
                    LoadSubLevel(subLevelPath, new Vector2(x * (int)SubLevelSize.Width, y * (int)SubLevelSize.Height));
                }
            }

            // Verify that the level has a beginning and an end.
            if (Player == null)
                throw new NotSupportedException("A level must have a starting point.");

            // Center camera on player
            SetCameraCenter(Player.Parameters.Position);

        }

        private void SetCameraCenter(Vector2 vector2)
        {
            cameraCenter = vector2;

            float minCameraX = (float)SubLevelSize.Width / 2f * (float)Tile.Width;
            float maxCameraX = Width * Tile.Width - minCameraX;
            float minCameraY = (float)SubLevelSize.Height / 2f * (float)Tile.Height;
            float maxCameraY = Height * Tile.Height - minCameraY;
            if (cameraCenter.X < minCameraX)
                cameraCenter.X = minCameraX;
            if (cameraCenter.X > maxCameraX)
                cameraCenter.X = maxCameraX;
            if (cameraCenter.Y < minCameraY)
                cameraCenter.Y = minCameraY;
            if (cameraCenter.Y > maxCameraY)
                cameraCenter.Y = maxCameraY;

        }

        private void LoadSubLevel(string path, Vector2 tileOffset)
        {
            using (StreamReader reader = new StreamReader(path))
            {
                string line = reader.ReadLine();
                while (line != null)
                {
                    if (line == "Tiles:")
                    {
                        LoadTiles(ReadLevelDataBlock(reader), tileOffset);
                    }
                    else if (line == "Enemy:")
                    {
                        // Skip enemies
                        LoadEnemy(ReadLevelParameterBlock(reader), tileOffset);
                    }
                    else if (line == "Boss:")
                    {
                        // Skip enemies
                        LoadBoss(ReadLevelParameterBlock(reader), tileOffset);
                    }
                    else if (line == "Platform:")
                    {
                        // Skip enemies
                        LoadPlatform(ReadLevelParameterBlock(reader), tileOffset);
                    }
                    else if (line == "Powerup:")
                    {
                        // Skip enemies
                        LoadPowerup(ReadLevelParameterBlock(reader), tileOffset);
                    }
                    line = reader.ReadLine();
                }
            }
        }

        private void LoadEnemy(Dictionary<string, string[]> dictionary, Vector2 tileOffset)
        {
            string[] values = dictionary["Position"];

            int x = int.Parse(values[0]) + (int)tileOffset.X;
            int y = int.Parse(values[1]) + (int)tileOffset.Y;


            Vector2 enemyStart = RectangleExtensions.GetBottomCenter(GetTileBounds(x, y));
            Car e = new Car(this, enemyStart, random.Next(6), random.Next(1000)/1000f);
            Enemies.Add(e);
        }

        private void LoadBoss(Dictionary<string, string[]> dictionary, Vector2 tileOffset)
        {
            string[] values = dictionary["Position"];
            int x = int.Parse(values[0]);
            int y = int.Parse(values[1]);
            Vector2 bossStart = new Vector2(x,y);
            values = dictionary["Type"];
            int type = int.Parse(values[0]);

            if (type == 1)
                boss = new Boss1(this, bossStart);
        }

        private void LoadPowerup(Dictionary<string, string[]> dictionary, Vector2 tileOffset)
        {
            string[] values = dictionary["Position"];
            int x = int.Parse(values[0]) + (int)tileOffset.X;
            int y = int.Parse(values[1]) + (int)tileOffset.Y;

            Vector2 position = RectangleExtensions.GetBottomCenter(GetTileBounds(x, y));
            Powerup p = new Powerup(this, position, (Powerup.Powerups)int.Parse(dictionary["Type"][0]), (float)random.Next(314) / 100f);
            Powerups.Add(p);
            
        }

        private void LoadPlatform(Dictionary<string, string[]> dictionary, Vector2 tileOffset)
        {
            string[] values;
            values = dictionary["Size"];
            int size = int.Parse(values[0]);
            values = dictionary["StartPosition"];
            int minX = int.Parse(values[0]);
            Vector2 startPosition = new Vector2(Tile.Width * (minX + (int)tileOffset.X), Tile.Height * (1 + int.Parse(values[1]) + (int)tileOffset.Y));
            startPosition += new Vector2(Tile.Width / 2f * (float)size, 0);
            values = dictionary["EndPosition"];
            int maxX = int.Parse(values[0]);
            Vector2 endPosition = new Vector2(Tile.Width * (maxX + (int)tileOffset.X), Tile.Height * (1 + int.Parse(values[1]) + (int)tileOffset.Y));
            endPosition += new Vector2(Tile.Width / 2f * (float)size, 0);
            values = dictionary["CurrentPosition"];
            int currentX = int.Parse(values[0]);
            float x = Tile.Width * (currentX + (int)tileOffset.X);
            x += 0.5f * (float)(size * Tile.Width);
            Vector2 currentPosition = new Vector2(x, Tile.Height * (1 + int.Parse(values[1]) + (int)tileOffset.Y));

            if (minX > currentX)
                throw new Exception("Platforms current index is less than the given minimum");
            if (maxX < currentX)
                throw new Exception("Platforms current index is larger than the given maximum");
            if (startPosition == endPosition)
                throw new Exception("Minimum and maximum values for platform makes it unable to move");

            Platform e = new Platform(Content, this, currentPosition, startPosition, endPosition, size);
            Platforms.Add(e);
        }


        private Dictionary<string, string[]> ReadLevelParameterBlock(StreamReader reader)
        {
            Dictionary<string, string[]> parameters = new Dictionary<string, string[]>();
            List<string> lines = ReadLevelDataBlock(reader);
            foreach (string line in lines)
            {
                int nameIndex = line.IndexOf('=');
                string name = line.Substring(0, nameIndex);
                string[] values = line.Substring(nameIndex + 1).Split(new char[] { ',' });
                parameters.Add(name, values);
            }
            return parameters;
        }

        private List<string> ReadLevelDataBlock(StreamReader reader)
        {
            List<string> lines = new List<string>();
            string line;
            while (true)
            {
                line = reader.ReadLine();
                if (line != "end")
                    lines.Add(line);
                else
                    break;
            }
            return lines;
        }

        /// <summary>
        /// Iterates over every tile in the structure file and loads its
        /// appearance and behavior. This method also validates that the
        /// file is well-formed with a player start point, exit, etc.
        /// </summary>
        /// <param name="path">
        /// The absolute path to the level file to be loaded.
        /// </param>
        private void LoadTiles(List<string> lines, Vector2 tileOffset)
        {
            // Loop over every tile position,
            for (int y = 0; y < SubLevelSize.Height; ++y)
            {
                for (int x = 0; x < SubLevelSize.Width; ++x)
                {
                    // to load each tile.
                    char tileType = lines[y][x];
                    tiles[x + (int)tileOffset.X, y + (int)tileOffset.Y] = LoadTile(tileType, x + (int)tileOffset.X, y + (int)tileOffset.Y);
                }
            }
        }

        /// <summary>
        /// Loads an individual tile's appearance and behavior.
        /// </summary>
        /// <param name="tileType">
        /// The character loaded from the structure file which
        /// indicates what should be loaded.
        /// </param>
        /// <param name="x">
        /// The X location of this tile in tile space.
        /// </param>
        /// <param name="y">
        /// The Y location of this tile in tile space.
        /// </param>
        /// <returns>The loaded tile.</returns>
        private Tile LoadTile(char tileType, int x, int y)
        {
            switch (tileType)
            {
                // Blank space
                case '.':
                    return new Tile(null, new Vector2(0, 0), TileCollision.Passable);

                // Exit boss trigger
                case 'b':
                    return new Tile(null, new Vector2(0, 0), TileCollision.ExitBossScreen);
                // Boss trigger
                case 'B':
                    return new Tile(null, new Vector2(0, 0), TileCollision.EnterBossScreen);

                // Player 1 start point
                case '1':
                    return LoadStartTile(x, y);

                // Impassable block
                case '#':
                    return LoadBlockTile(new Vector2(0, 0), TileCollision.Impassable);
                case 'O':
                    return LoadBlockTile(new Vector2(1+random.Next(3), 0), TileCollision.Impassable);
                case 'i':
                    return LoadBlockTile(new Vector2(0, 1), TileCollision.Impassable);
                case 'p':
                    return LoadBlockTile(new Vector2(1, 1), TileCollision.Impassable);
                case 'I':
                    return LoadBlockTile(new Vector2(2, 1), TileCollision.Passable);
               



                // Unknown tile type character
                default:
                    throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", tileType, x, y));
            }
        }




        /// <summary>
        /// Loads a tile with a random appearance.
        /// </summary>
        /// <param name="baseName">
        /// The content name prefix for this group of tile variations. Tile groups are
        /// name LikeThis0.png and LikeThis1.png and LikeThis2.png.
        /// </param>
        /// <param name="variationCount">
        /// The number of variations in this group.
        /// </param>
        private Tile LoadBlockTile(Vector2 spriteIndex, TileCollision collision)
        {
            if (Constants.UseBlocks)
            {
                if (spriteIndex.X >= 2 && spriteIndex.Y == 0)
                    return new Tile(null, new Vector2(0, 0), collision);
                else
                    return new Tile(blocksTexture, new Vector2(0, 6), collision);

            }
            else
                return new Tile(blocksTexture, spriteIndex, collision);
        }


        /// <summary>
        /// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
        /// </summary>
        private Tile LoadStartTile(int x, int y)
        {
            if (Player != null)
                throw new NotSupportedException("A level may only have one starting point.");

            start = RectangleExtensions.GetBottomCenter(GetTileBounds(x, y));
            player = new Player(this, start);
            player.EnterBossScreen += new BossScreenDelegate(player_EnterBossScreen);
            player.ExitBossScreen += new BossScreenDelegate(player_ExitBossScreen);

            return new Tile(null, new Vector2(0, 0), TileCollision.Passable);
        }

        void player_ExitBossScreen(GameObject sourceObject, int triggerTileX, int triggerTileY)
        {
            Boss.UnInitialize();
            FixedCamera = false;
            PlatformerGame.StartBackgroundMusic(PlatformerGame.GameMusic.InGameMusic);
        }

        void player_EnterBossScreen(GameObject sourceObject, int triggerTileX, int triggerTileY)
        {
            // Get screen offset
            Vector2 screenOffset = new Vector2(triggerTileX / SubLevelSize.Width, triggerTileY / SubLevelSize.Height);
            screenOffset += new Vector2((SubLevelSize.Width - 2) * Tile.Width / 2f, SubLevelSize.Height * Tile.Height / 2f);
            CameraFixPosition = screenOffset;
            FixedCamera = true;
            Boss.Initialize();
            PlatformerGame.StartBackgroundMusic(PlatformerGame.GameMusic.BossMusic);
        }

        /// <summary>
        /// Unloads the level content.
        /// </summary>
        public void Dispose()
        {
            Content.Unload();
        }

        #endregion

        #region Bounds and collision

        /// <summary>
        /// Gets the collision mode of the tile at a particular location.
        /// This method handles tiles outside of the levels boundries by making it
        /// impossible to escape past the left or right edges, but allowing things
        /// to jump beyond the top of the level and fall off the bottom.
        /// </summary>
        public TileCollision GetCollision(int x, int y)
        {
            // Prevent escaping past the level ends.
            if (x < 0 || x >= Width)
                return TileCollision.Impassable;
            // Allow jumping past the level top and falling through the bottom.
            if (y < 0 || y >= Height)
                return TileCollision.Passable;

            return tiles[x, y].Collision;
        }

        /// <summary>
        /// Gets the bounding rectangle of a tile in world space.
        /// </summary>        
        public Rectangle GetTileBounds(int x, int y)
        {
            return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
        }

        /// <summary>
        /// Width of level measured in tiles.
        /// </summary>
        public int Width { get; set; }

        /// <summary>
        /// Height of the level measured in tiles.
        /// </summary>
        public int Height { get; set; }

        #endregion

        #region Update

        /// <summary>
        /// Updates all objects in the world, performs collision between them,
        /// and handles the time limit with scoring.
        /// </summary>
        public void Update(GameTime gameTime)
        {
            if (Paused)
                return;

            foreach (Platform p in Platforms)
            {
                p.Update(gameTime);
            }

            foreach (Car enemy in Enemies)
            {
                enemy.Update(gameTime);
            }

            foreach (Powerup p in Powerups)
            {
                p.Update(gameTime);
            }

            boss.Update(gameTime);
            Player.Update(gameTime);

            Explosion[] explosionArray = Explosions.ToArray();
            for (int i = 0; i < explosionArray.Length; i++)
            {
                Explosion explosion = explosionArray[i];
                explosion.Update(gameTime);
                if (!explosion.IsAlive)
                    Explosions.Remove(explosion);
            }




            // Camera will try to center player if he is 6 tiles away from screen center
            Vector2 cameraDestination;
            if (!FixedCamera)
            {
                cameraDestination = Player.Parameters.Position;
                UpdateCamera(cameraDestination, gameTime, 1);
            }
            else
            {
                cameraDestination = CameraFixPosition;
                UpdateCamera(cameraDestination, gameTime, 0);
            }

        }

        private void UpdateCamera(Vector2 cameraDestination, GameTime gameTime, int focusRadius)
        {
            float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
            Vector2 cameraPlayerDistance = cameraDestination - cameraCenter;
            float minDistanceToStopCamera;
            float distanceLength = cameraPlayerDistance.Length();

            if (distanceLength > (float)Tile.Width)
            {
                if (!cameraWasMoved)
                    minDistanceToStopCamera = (float)Tile.Width * 5;
                else
                    minDistanceToStopCamera = (float)Tile.Width * focusRadius;

                if (distanceLength < minDistanceToStopCamera)
                    cameraWasMoved = false;
                else
                {
                    cameraWasMoved = true;
                    float cameraSpeed = MathHelper.Clamp(cameraPlayerDistance.Length() * 3f * elapsedTime, 2, 5000);
                    if (cameraSpeed >= distanceLength)
                    {
                        SetCameraCenter(CameraFixPosition);
                    }
                    else
                    {
                        cameraPlayerDistance.Normalize();
                        Vector2 translate = cameraPlayerDistance * cameraSpeed;
                        SetCameraCenter(cameraCenter + translate);
                    }
                }
            }
            else
            {
                cameraWasMoved = false;
            }
        }

        /// <summary>
        /// Restores the player to the starting point to try the level again.
        /// </summary>
        public void StartNewLife()
        {
            Player.Reset(start);
        }

        #endregion

        #region Draw

        /// <summary>
        /// Draw everything in the level from background to foreground.
        /// </summary>
        public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            DrawTiles(spriteBatch);

            foreach (Platform p in Platforms)
            {
                p.Draw(gameTime, spriteBatch);
            }
            foreach (Car enemy in Enemies)
            {
                enemy.Draw(gameTime, spriteBatch);
            }
            foreach (Powerup p in Powerups)
            {
                p.Draw(gameTime, spriteBatch);
            }
            boss.Draw(gameTime, spriteBatch);
            Player.Draw(gameTime, spriteBatch);
            foreach (Explosion explosion in Explosions)
            {
                explosion.Draw(gameTime, spriteBatch);
            }
        }

        /// <summary>
        /// Draws each tile in the level.
        /// </summary>
        private void DrawTiles(SpriteBatch spriteBatch)
        {
            int tileOffsetX = (int)Math.Floor(ScreenX / (float)Tile.Width);
            int tileOffsetY = (int)Math.Floor(ScreenY / (float)Tile.Height);


            // For each tile position
            for (int y = tileOffsetY - 1; y < tileOffsetY + SubLevelSize.Height + 1; ++y)
            {
                for (int x = tileOffsetX - 1; x < tileOffsetX + SubLevelSize.Width + 1; ++x)
                {
                    if (x < 0 || y < 0 || x >= Width || y >= Height)
                        continue;

                    // If there is a visible tile in that position
                    Texture2D texture = tiles[x, y].Texture;
                    if (texture != null)
                    {
                        // Draw it in screen space.
                        Vector2 position = new Vector2(x, y) * Tile.Size;
                        Color color;
                        color = new Color(Color.White, Constants.Alpha);
                        spriteBatch.Draw(texture, position, tiles[x, y].SourceRectangle, color);
                    }
                }
            }

        }

        #endregion

        internal void Explode(Vector2 vector2)
        {
            Explosions.Add(new Explosion(this, vector2, 0));
        }

        internal void ExplodeInsane(Vector2 vector2, int radius, int count)
        {
            for (int i = 0; i < count; i++)
            {
                int x = random.Next(2 * radius) - radius;
                int y = random.Next(2 * radius) - radius;
                const int maxDuration = 2000;
                float delay = ((float)random.Next((i * maxDuration) / count, maxDuration) / 1000.0f);
                Explosions.Add(new Explosion(this, new Vector2(vector2.X + x, vector2.Y + y), delay));
            }
        }

        internal void ExplodeInsane(Rectangle rectangle, int count)
        {
            for (int i = 0; i < count; i++)
            {
                int x = random.Next(rectangle.Width);
                int y = (int)((float)i*(float)rectangle.Height / (float)count);
                const int maxDuration = 2000;
                float delay = ((float)random.Next((i * maxDuration) / count, maxDuration) / 1000.0f);
                Explosions.Add(new Explosion(this, new Vector2(rectangle.X + x, rectangle.Y + y), delay));
                Explosions.Add(new Explosion(this, new Vector2(rectangle.X + x, rectangle.Y - y+rectangle.Height), delay));
            }
        }

        public void SoundFXExplode()
        {
            PlatformerGame.Sound.Play2D(@"Sound\hit1.mp3");
        }

        public void SoundFXShot()
        {
            PlatformerGame.Sound.Play2D(@"Sound\shot.mp3");
        }

       
    }
}
